window.ls = window.ls || {};
window.ls.container = function() {
    let stock = {};
    let listeners = {};
    let namespaces = {};
    let set = function(name, object, singleton, watch = true) {
        if (typeof name !== 'string') {
            throw new Error('var name must be of type string');
        }
        if (typeof singleton !== 'boolean') {
            throw new Error('var singleton "' + singleton + '" of service "' + name + '" must be of type boolean');
        }
        stock[name] = {
            name: name,
            object: object,
            singleton: singleton,
            instance: null,
            watch: watch,
        };
        if (!watch) {
            return this;
        }
        let binds = listeners[name] || {};
        for (let key in binds) {
            if (binds.hasOwnProperty(key)) {
                document.dispatchEvent(new CustomEvent(key));
            }
        }
        return this;
    };
    let get = function(name) {
        let service = (undefined !== stock[name]) ? stock[name] : null;
        if (null == service) {
            return null;
        }
        if (service.instance) {
            return service.instance;
        }
        let instance = (typeof service.object === 'function') ? this.resolve(service.object) : service.object;
        let skip = false;
        if (service.watch && name !== 'window' && name !== 'document' && name !== 'element' && typeof instance === 'object' && instance !== null) {
            let handler = {
                name: service.name,
                watch: function() {},
                get: function(target, key) {
                    if (key === "__name") {
                        return this.name;
                    }
                    if (key === "__watch") {
                        return this.watch;
                    }
                    if (key === "__proxy") {
                        return true;
                    }
                    if (typeof target[key] === 'object' && target[key] !== null && !target[key].__proxy) {
                        let handler = Object.assign({}, this);
                        handler.name = handler.name + '.' + key;
                        return new Proxy(target[key], handler)
                    } else {
                        return target[key];
                    }
                },
                set: function(target, key, value, receiver) {
                    if (key === "__name") {
                        return this.name = value;
                    }
                    if (key === "__watch") {
                        return this.watch = value;
                    }
                    target[key] = value;
                    let path = receiver.__name + '.' + key;
                    document.dispatchEvent(new CustomEvent(path + '.changed'));
                    if (skip) {
                        return true;
                    }
                    skip = true;
                    container.set('$prop', key, true);
                    container.set('$value', value, true);
                    container.resolve(this.watch);
                    container.set('$key', null, true);
                    container.set('$value', null, true);
                    skip = false;
                    return true;
                },
            };
            instance = new Proxy(instance, handler);
        }
        if (service.singleton) {
            service.instance = instance;
        }
        return instance;
    };
    let resolve = function(target) {
        if (!target) {
            return () => {};
        }
        let self = this;
        const REGEX_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
        const REGEX_FUNCTION_PARAMS = /(?:\s*(?:function\s*[^(]*)?\s*)((?:[^'"]|(?:(?:(['"])(?:(?:.*?[^\\]\2)|\2))))*?)\s*(?=(?:=>)|{)/m;
        const REGEX_PARAMETERS_VALUES = /\s*([\w\\$]+)\s*(?:=\s*((?:(?:(['"])(?:\3|(?:.*?[^\\]\3)))((\s*\+\s*)(?:(?:(['"])(?:\6|(?:.*?[^\\]\6)))|(?:[\w$]*)))*)|.*?))?\s*(?:,|$)/gm;

        function getParams(func) {
            let functionAsString = func.toString();
            let params = [];
            let match;
            functionAsString = functionAsString.replace(REGEX_COMMENTS, '');
            functionAsString = functionAsString.match(REGEX_FUNCTION_PARAMS)[1];
            if (functionAsString.charAt(0) === '(') {
                functionAsString = functionAsString.slice(1, -1);
            }
            while (match = REGEX_PARAMETERS_VALUES.exec(functionAsString)) {
                params.push(match[1]);
            }
            return params;
        }
        let args = getParams(target);
        return target.apply(target, args.map(function(value) {
            return self.get(value.trim());
        }));
    };
    let path = function(path, value, type) {
        type = (type) ? type : 'assign';
        path = container.scope(path).split('.');
        let name = path.shift();
        let object = container.get(name);
        let result = null;
        while (path.length > 1) {
            if (!object) {
                return null;
            }
            object = object[path.shift()];
        }
        let shift = path.shift();
        if (value !== null && value !== undefined && object && shift && (object[shift] !== undefined || object[shift] !== null)) {
            switch (type) {
                case 'append':
                    if (!Array.isArray(object[shift])) {
                        object[shift] = [];
                    }
                    object[shift].push(value);
                    break;
                case 'prepend':
                    if (!Array.isArray(object[shift])) {
                        object[shift] = [];
                    }
                    object[shift].unshift(value);
                    break;
                case 'splice':
                    if (!Array.isArray(object[shift])) {
                        object[shift] = [];
                    }
                    object[shift].splice(value, 1);
                    break;
                default:
                    object[shift] = value;
            }
            return true;
        }
        if (!object) {
            return null;
        }
        if (!shift) {
            result = object;
        } else {
            return object[shift];
        }
        return result;
    };
    let bind = function(element, path, callback) {
        let event = container.scope(path) + '.changed';
        let service = event.split('.').slice(0, 1).pop();
        let debug = element.getAttribute('data-debug') || false;
        listeners[service] = listeners[service] || {};
        listeners[service][event] = true;
        let printer = (function(x) {
            return function() {
                if (!document.body.contains(element)) {
                    element = null;
                    document.removeEventListener(event, printer, false);
                    return false;
                }
                let oldNamespaces = namespaces;
                namespaces = x;
                callback();
                namespaces = oldNamespaces;
            }
        }(Object.assign({}, namespaces)));
        document.addEventListener(event, printer);
    };
    let addNamespace = function(key, scope) {
        namespaces[key] = scope;
        return this;
    }
    let removeNamespace = function(key) {
        delete namespaces[key];
        return this;
    }
    let scope = function(path) {
        for (let [key, value] of Object.entries(namespaces)) {
            path = (path.indexOf('.') > -1) ? path.replace(key + '.', value + '.') : path.replace(key, value);
        }
        return path;
    }
    let container = {
        set: set,
        get: get,
        resolve: resolve,
        path: path,
        bind: bind,
        scope: scope,
        addNamespace: addNamespace,
        removeNamespace: removeNamespace,
        stock: stock,
        listeners: listeners,
        namespaces: namespaces,
    };
    set('container', container, true, false);
    return container;
}();
window.ls.container.set('http', function(document) {
    let globalParams = [],
        globalHeaders = [];
    let addParam = function(url, param, value) {
        param = encodeURIComponent(param);
        let a = document.createElement('a');
        param += (value ? "=" + encodeURIComponent(value) : "");
        a.href = url;
        a.search += (a.search ? "&" : "") + param;
        return a.href;
    };
    let request = function(method, url, headers, payload, progress) {
        let i;
        if (-1 === ['GET', 'POST', 'PUT', 'DELETE', 'TRACE', 'HEAD', 'OPTIONS', 'CONNECT', 'PATCH'].indexOf(method)) {
            throw new Error('var method must contain a valid HTTP method name');
        }
        if (typeof url !== 'string') {
            throw new Error('var url must be of type string');
        }
        if (typeof headers !== 'object') {
            throw new Error('var headers must be of type object');
        }
        if (typeof url !== 'string') {
            throw new Error('var url must be of type string');
        }
        for (i = 0; i < globalParams.length; i++) {
            url = addParam(url, globalParams[i].key, globalParams[i].value);
        }
        return new Promise(function(resolve, reject) {
            let xmlhttp = new XMLHttpRequest();
            xmlhttp.open(method, url, true);
            xmlhttp.setRequestHeader('Ajax', '1');
            for (i = 0; i < globalHeaders.length; i++) {
                xmlhttp.setRequestHeader(globalHeaders[i].key, globalHeaders[i].value);
            }
            for (let key in headers) {
                if (headers.hasOwnProperty(key)) {
                    xmlhttp.setRequestHeader(key, headers[key]);
                }
            }
            xmlhttp.onload = function() {
                if (4 === xmlhttp.readyState && 200 === xmlhttp.status) {
                    resolve(xmlhttp.response);
                } else {
                    document.dispatchEvent(new CustomEvent('http-' + method.toLowerCase() + '-' + xmlhttp.status));
                    reject(new Error(xmlhttp.statusText));
                }
            };
            if (progress) {
                xmlhttp.addEventListener('progress', progress);
                xmlhttp.upload.addEventListener('progress', progress, false);
            }
            xmlhttp.onerror = function() {
                reject(new Error("Network Error"));
            };
            xmlhttp.send(payload);
        })
    };
    return {
        'get': function(url) {
            return request('GET', url, {}, '')
        },
        'post': function(url, headers, payload) {
            return request('POST', url, headers, payload)
        },
        'put': function(url, headers, payload) {
            return request('PUT', url, headers, payload)
        },
        'patch': function(url, headers, payload) {
            return request('PATCH', url, headers, payload)
        },
        'delete': function(url) {
            return request('DELETE', url, {}, '')
        },
        'addGlobalParam': function(key, value) {
            globalParams.push({
                key: key,
                value: value
            });
        },
        'addGlobalHeader': function(key, value) {
            globalHeaders.push({
                key: key,
                value: value
            });
        }
    }
}, true, false);
window.ls.container.set('cookie', function(document) {
    function get(name) {
        let value = "; " + document.cookie,
            parts = value.split("; " + name + "=");
        if (parts.length === 2) {
            return parts.pop().split(";").shift();
        }
        return null;
    }

    function set(name, value, days) {
        let date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        let expires = (0 < days) ? 'expires=' + date.toUTCString() : 'expires=0';
        document.cookie = name + "=" + value + ";" + expires + ";path=/";
        return this;
    }
    return {
        'get': get,
        'set': set
    }
}, true, false);
window.ls.container.set('view', function(http, container) {
    let stock = {};
    let execute = function(view, node, container) {
        container.set('element', node, true, false);
        container.resolve(view.controller);
        if (true !== view.repeat) {
            node.removeAttribute(view.selector);
        }
    };
    let parse = function(node, skip, callback) {
        if (node.tagName === 'SCRIPT') {
            return;
        }
        if (node.attributes && skip !== true) {
            let attrs = [];
            let attrsLen = node.attributes.length;
            for (let x = 0; x < attrsLen; x++) {
                attrs.push(node.attributes[x].nodeName);
            }
            if (1 !== node.nodeType) {
                return;
            }
            if (attrs && attrsLen) {
                for (let x = 0; x < attrsLen; x++) {
                    if (node.$lsSkip === true) {
                        break;
                    }
                    let pointer = (!/Edge/.test(navigator.userAgent)) ? x : (attrsLen - 1) - x;
                    let length = attrsLen;
                    let attr = attrs[pointer];
                    if (!stock[attr]) {
                        continue;
                    }
                    let comp = stock[attr];
                    if (typeof comp.template === "function") {
                        comp.template = container.resolve(comp.template);
                    }
                    if (!comp.template) {
                        (function(comp, node, container) {
                            execute(comp, node, container);
                        })(comp, node, container);
                        if (length !== attrsLen) {
                            x--;
                        }
                        if (callback) {
                            callback();
                        }
                        continue;
                    }
                    node.classList.remove('load-end');
                    node.classList.add('load-start');
                    node.$lsSkip = true;
                    http.get(comp.template).then(function(node, comp) {
                        return function(data) {
                            node.$lsSkip = false;
                            node.innerHTML = data;
                            node.classList.remove('load-start');
                            node.classList.add('load-end');
                            (function(comp, node, container) {
                                execute(comp, node, container);
                            })(comp, node, container);
                            parse(node, true);
                            if (callback) {
                                callback();
                            }
                        }
                    }(node, comp), function(error) {
                        throw new Error('Failed to load comp template: ' + error.message);
                    });
                }
            }
        }
        if (true === node.$lsSkip) {
            return;
        }
        let list = (node) ? node.childNodes : [];
        if (node.$lsSkip === true) {
            list = [];
        }
        for (let i = 0; i < list.length; i++) {
            let child = list[i];
            parse(child);
        }
    };
    return {
        stock: stock,
        add: function(object) {
            if (typeof object !== 'object') {
                throw new Error('object must be of type object');
            }
            let defaults = {
                'selector': '',
                'controller': function() {},
                'template': '',
                'repeat': false,
                'protected': false
            };
            for (let prop in defaults) {
                if (!defaults.hasOwnProperty(prop)) {
                    continue;
                }
                if (prop in object) {
                    continue;
                }
                object[prop] = defaults[prop];
            }
            if (!object.selector) {
                throw new Error('View component is missing a selector attribute');
            }
            stock[object.selector] = object;
            return this;
        },
        render: function(element, callback) {
            parse(element, false, callback);
            element.dispatchEvent(new window.Event('rendered', {
                bubbles: false
            }));
        }
    }
}, true, false);
window.ls.container.set('router', function(window) {
    let getJsonFromUrl = function(URL) {
        let query;
        if (URL) {
            let pos = location.search.indexOf('?');
            if (pos === -1) return [];
            query = location.search.substr(pos + 1);
        } else {
            query = location.search.substr(1);
        }
        let result = {};
        query.split('&').forEach(function(part) {
            if (!part) {
                return;
            }
            part = part.split('+').join(' ');
            let eq = part.indexOf('=');
            let key = eq > -1 ? part.substr(0, eq) : part;
            let val = eq > -1 ? decodeURIComponent(part.substr(eq + 1)) : '';
            let from = key.indexOf('[');
            if (from === -1) {
                result[decodeURIComponent(key)] = val;
            } else {
                let to = key.indexOf(']');
                let index = decodeURIComponent(key.substring(from + 1, to));
                key = decodeURIComponent(key.substring(0, from));
                if (!result[key]) {
                    result[key] = [];
                }
                if (!index) {
                    result[key].push(val);
                } else {
                    result[key][index] = val;
                }
            }
        });
        return result;
    };
    let states = [];
    let params = getJsonFromUrl(window.location.search);
    let hash = window.location.hash;
    let current = null;
    let previous = null;
    let getPrevious = () => previous;
    let getCurrent = () => current;
    let setPrevious = (value) => {
        previous = value;
        return this;
    };
    let setCurrent = (value) => {
        current = value;
        return this;
    };
    let setParam = function(key, value) {
        params[key] = value;
        return this;
    };
    let getParam = function(key, def) {
        if (key in params) {
            return params[key];
        }
        return def;
    };
    let getParams = function() {
        return params;
    };
    let getURL = function() {
        return window.location.href;
    };
    let add = function(path, view) {
        if (typeof path !== 'string') {
            throw new Error('path must be of type string');
        }
        if (typeof view !== 'object') {
            throw new Error('view must be of type object');
        }
        states[states.length++] = {
            path: path,
            view: view
        };
        return this;
    };
    let match = function(location) {
        let url = location.pathname;
        states.sort(function(a, b) {
            return b.path.length - a.path.length;
        });
        states.sort(function(a, b) {
            let n = b.path.split('/').length - a.path.split('/').length;
            if (n !== 0) {
                return n;
            }
            return b.path.length - a.path.length;
        });
        for (let i = 0; i < states.length; i++) {
            let value = states[i];
            value.path = (value.path.substring(0, 1) !== '/') ? location.pathname + value.path : value.path;
            let match = new RegExp("^" + value.path.replace(/:[^\s/]+/g, '([\\w-]+)') + "$");
            let found = url.match(match);
            if (found) {
                previous = current;
                current = value;
                return value;
            }
        }
        return null
    };
    let change = function(URL, replace) {
        if (!replace) {
            window.history.pushState({}, '', URL);
        } else {
            window.history.replaceState({}, '', URL);
        }
        window.dispatchEvent(new PopStateEvent('popstate', {}));
        return this;
    };
    let reload = function() {
        return change(window.location.href);
    };
    return {
        setParam: setParam,
        getParam: getParam,
        getParams: getParams,
        getURL: getURL,
        add: add,
        change: change,
        reload: reload,
        match: match,
        getCurrent: getCurrent,
        setCurrent: setCurrent,
        getPrevious: getPrevious,
        setPrevious: setPrevious,
        params: params,
        hash: hash,
        reset: function() {
            this.params = getJsonFromUrl(window.location.search);
            this.hash = window.location.hash;
        }
    };
}, true, true);
window.ls.container.set('expression', function(container, filter) {
    let paths = [];
    return {
        regex: /(\{{.*?\}})/gi,
        parse: function(string, def, cast = false) {
            def = def || '';
            paths = [];
            return string.replace(this.regex, match => {
                let reference = match.substring(2, match.length - 2).replace('[\'', '.').replace('\']', '').trim();
                reference = reference.split('|');
                let path = container.scope((reference[0] || ''));
                let result = container.path(path);
                path = container.scope(path);
                if (!paths.includes(path)) {
                    paths.push(path);
                }
                if (reference.length >= 2) {
                    for (let i = 1; i < reference.length; i++) {
                        result = filter.apply(reference[i], result);
                    }
                }
                if (null === result || undefined === result) {
                    result = def;
                } else if (typeof result === 'object') {
                    result = JSON.stringify(result, null, 4);
                } else if (((typeof result === 'object') || (typeof result === 'string')) && cast) {
                    result = '\'' + result + '\'';
                }
                return result;
            }).replace(/\\{/g, "{").replace(/\\}/g, "}");
        },
        getPaths: () => paths,
    }
}, true, false);
window.ls.container.set('filter', function(container) {
    let filters = {};
    let add = function(name, callback) {
        filters[name] = callback;
        return this;
    };
    let apply = function(name, value) {
        container.set('$value', value, true, false);
        return container.resolve(filters[name]);
    };
    add('uppercase', ($value) => {
        if (typeof $value !== 'string') {
            return $value;
        }
        return $value.toUpperCase();
    });
    add('lowercase', ($value) => {
        if (typeof $value !== 'string') {
            return $value;
        }
        return $value.toLowerCase();
    });
    return {
        add: add,
        apply: apply
    }
}, true, false);
window.ls.container.get('filter').add('escape', value => {
    if (typeof value !== 'string') {
        return value;
    }
    return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;').replace(/\'/g, '&#39;').replace(/\//g, '&#x2F;');
});
window.ls = window.ls || {};
window.ls.container.set('window', window, true, false).set('document', window.document, true, false).set('element', window.document, true, false);
window.ls.run = function(window) {
    try {
        this.view.render(window.document);
    } catch (error) {
        let handler = window.ls.container.resolve(this.error);
        handler(error);
    }
};
window.ls.error = () => {
    return error => {
        console.error('ls-error', error.message, error.stack, error.toString());
    }
};
window.ls.router = window.ls.container.get('router');
window.ls.view = window.ls.container.get('view');
window.ls.filter = window.ls.container.get('filter');
window.ls.container.get('view').add({
    selector: 'data-ls-router',
    repeat: false,
    controller: function(element, window, document, view, router) {
        let firstFromServer = (element.getAttribute('data-first-from-server') === 'true');
        let scope = {
            selector: 'data-ls-scope',
            template: false,
            repeat: true,
            controller: function() {},
        };
        let init = function(route) {
            let count = parseInt(element.getAttribute('data-ls-scope-count') || 0);
            element.setAttribute('data-ls-scope-count', count + 1);
            window.scrollTo(0, 0);
            if (window.document.body.scrollTo) {
                window.document.body.scrollTo(0, 0);
            }
            router.reset();
            if (null === route) {
                return;
            }
            scope.template = (undefined !== route.view.template) ? route.view.template : null;
            scope.controller = (undefined !== route.view.controller) ? route.view.controller : function() {};
            document.dispatchEvent(new CustomEvent('state-change'));
            if (firstFromServer && null === router.getPrevious()) {
                scope.template = '';
                document.dispatchEvent(new CustomEvent('state-changed'));
            } else if (count === 1) {
                view.render(element, function() {
                    document.dispatchEvent(new CustomEvent('state-changed'));
                });
            } else if (null !== router.getPrevious()) {
                view.render(element, function() {
                    document.dispatchEvent(new CustomEvent('state-changed'));
                });
            }
        };
        let findParent = function(tagName, el) {
            if ((el.nodeName || el.tagName).toLowerCase() === tagName.toLowerCase()) {
                return el;
            }
            while (el = el.parentNode) {
                if ((el.nodeName || el.tagName).toLowerCase() === tagName.toLowerCase()) {
                    return el;
                }
            }
            return null;
        };
        element.removeAttribute('data-ls-router');
        element.setAttribute('data-ls-scope', '');
        element.setAttribute('data-ls-scope-count', 1);
        view.add(scope);
        document.addEventListener('click', function(event) {
            let target = findParent('a', event.target);
            if (!target) {
                return false;
            }
            if (!target.href) {
                return false;
            }
            if ((event.metaKey)) {
                return false;
            }
            if ((target.hasAttribute('target')) && ('_blank' === target.getAttribute('target'))) {
                return false;
            }
            if (target.hostname !== window.location.hostname) {
                return false;
            }
            let route = router.match(target);
            if (null === route) {
                return false;
            }
            event.preventDefault();
            if (window.location === target.href) {
                return false;
            }
            route.view.state = (undefined === route.view.state) ? true : route.view.state;
            if (true === route.view.state) {
                if (router.getPrevious() && router.getPrevious().view && (router.getPrevious().view.scope !== route.view.scope)) {
                    window.location.href = target.href;
                    return false;
                }
                window.history.pushState({}, 'Unknown', target.href);
            }
            init(route);
            return true;
        });
        window.addEventListener('popstate', function() {
            init(router.match(window.location));
        });
        window.addEventListener('hashchange', function() {
            init(router.match(window.location));
        });
        init(router.match(window.location));
    }
});
window.ls.container.get('view').add({
    selector: 'data-ls-attrs',
    controller: function(element, expression, container) {
        let attrs = element.getAttribute('data-ls-attrs').trim().split(',');
        let paths = [];
        let debug = element.getAttribute('data-debug') || false;
        let check = () => {
            container.set('element', element, true, false);
            if (debug) {
                console.info('debug-ls-attrs attributes:', attrs);
            }
            for (let i = 0; i < attrs.length; i++) {
                let attr = attrs[i];
                let key = expression.parse((attr.substring(0, attr.indexOf('=')) || attr));
                paths = paths.concat(expression.getPaths());
                let value = '';
                if (attr.indexOf('=') > -1) {
                    value = expression.parse(attr.substring(attr.indexOf('=') + 1)) || '';
                    paths = paths.concat(expression.getPaths());
                }
                if (!key) {
                    return null;
                }
                element.setAttribute(key, value);
            }
        };
        check();
        for (let i = 0; i < paths.length; i++) {
            let path = paths[i].split('.');
            while (path.length) {
                container.bind(element, path.join('.'), check);
                path.pop();
            }
        }
    }
});
window.ls.container.get('view').add({
    selector: 'data-ls-bind',
    controller: function(element, expression, container) {
        let debug = element.getAttribute('data-debug') || false;
        let echo = function(value, bind = true) {
            if (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'BUTTON' || element.tagName === 'TEXTAREA') {
                let type = element.getAttribute('type');
                if ('radio' === type) {
                    if (value.toString() === element.value) {
                        element.setAttribute('checked', 'checked');
                    } else {
                        element.removeAttribute('checked');
                    }
                    if (bind) {
                        element.addEventListener('change', () => {
                            for (let i = 0; i < paths.length; i++) {
                                if (element.checked) {
                                    value = element.value;
                                }
                                container.path(paths[i], value);
                            }
                        });
                    }
                    return;
                }
                if ('checkbox' === type) {
                    if (typeof value === 'boolean' || value === 'true' || value === 'false') {
                        if (value === true || value === 'true') {
                            element.setAttribute('checked', 'checked');
                            element.checked = true;
                        } else {
                            element.removeAttribute('checked');
                            element.checked = false;
                        }
                    } else {
                        try {
                            value = JSON.parse(value);
                            element.checked = (Array.isArray(value) && (value.indexOf(element.value) > -1));
                            value = element.value;
                        } catch {
                            return null;
                        }
                    }
                    if (bind) {
                        element.addEventListener('change', () => {
                            for (let i = 0; i < paths.length; i++) {
                                let value = container.path(paths[i]);
                                let index = value.indexOf(element.value);
                                if (element.checked && index < 0) {
                                    value.push(element.value);
                                }
                                if (!element.checked && index > -1) {
                                    value.splice(index, 1);
                                }
                                container.path(paths[i], value);
                            }
                        });
                    }
                    return;
                }
                if (element.value !== value) {
                    element.value = value;
                    element.dispatchEvent(new Event('change'));
                }
                if (bind) {
                    element.addEventListener('input', sync);
                    element.addEventListener('change', sync);
                }
            } else {
                if (element.textContent != value) {
                    element.innerHTML = value;
                }
            }
        };
        let sync = (() => {
            return () => {
                if (debug) {
                    console.info('debug-ls-bind', 'sync-path', paths);
                    console.info('debug-ls-bind', 'sync-syntax', syntax);
                    console.info('debug-ls-bind', 'sync-syntax-parsed', parsedSyntax);
                    console.info('debug-ls-bind', 'sync-value', element.value);
                }
                for (let i = 0; i < paths.length; i++) {
                    if ('{{' + paths[i] + '}}' !== parsedSyntax) {
                        if (debug) {
                            console.info('debug-ls-bind', 'sync-skipped-path', paths[i]);
                            console.info('debug-ls-bind', 'sync-skipped-syntax', syntax);
                            console.info('debug-ls-bind', 'sync-skipped-syntax-parsed', parsedSyntax);
                        }
                        continue;
                    }
                    if (debug) {
                        console.info('debug-ls-bind', 'sync-loop-path', paths[i]);
                        console.info('debug-ls-bind', 'sync-loop-syntax', parsedSyntax);
                    }
                    container.path(paths[i], element.value);
                }
            }
        })();
        let syntax = element.getAttribute('data-ls-bind');
        let parsedSyntax = container.scope(syntax);
        let unsync = (!!element.getAttribute('data-unsync')) || false;
        let result = expression.parse(syntax);
        let paths = expression.getPaths();
        echo(result, !unsync);
        element.addEventListener('looped', function() {
            echo(expression.parse(parsedSyntax), false);
        });
        for (let i = 0; i < paths.length; i++) {
            let path = paths[i].split('.');
            if (debug) {
                console.info('debug-ls-bind', 'bind-path', path);
                console.info('debug-ls-bind', 'bind-syntax', syntax);
            }
            while (path.length) {
                container.bind(element, path.join('.'), () => {
                    echo(expression.parse(parsedSyntax), false);
                });
                path.pop();
            }
        }
    }
});
window.ls.container.get('view').add({
    selector: 'data-ls-if',
    controller: function(element, expression, container, view) {
        let result = '';
        let syntax = element.getAttribute('data-ls-if') || '';
        let debug = element.getAttribute('data-debug') || false;
        let paths = [];
        let check = () => {
            if (debug) {
                console.info('debug-ls-if', expression.parse(syntax.replace(/(\r\n|\n|\r)/gm, ' '), 'undefined', true));
            }
            try {
                result = (eval(expression.parse(syntax.replace(/(\r\n|\n|\r)/gm, ' '), 'undefined', true)));
            } catch (error) {
                throw new Error('Failed to evaluate expression "' + syntax + ' (resulted with: "' + result + '")": ' + error);
            }
            if (debug) {
                console.info('debug-ls-if result:', result);
            }
            paths = expression.getPaths();
            let prv = element.$lsSkip;
            element.$lsSkip = !result;
            if (!result) {
                element.style.visibility = 'hidden';
                element.style.display = 'none';
            } else {
                element.style.removeProperty('display');
                element.style.removeProperty('visibility');
            }
            if (prv === true && element.$lsSkip === false) {
                view.render(element)
            }
        };
        check();
        for (let i = 0; i < paths.length; i++) {
            let path = paths[i].split('.');
            while (path.length) {
                container.bind(element, path.join('.'), check);
                path.pop();
            }
        }
    }
});
window.ls.container.get('view').add({
    selector: 'data-ls-loop',
    template: false,
    repeat: false,
    nested: false,
    controller: function(element, view, container, window, expression) {
        let expr = expression.parse(element.getAttribute('data-ls-loop'));
        let as = element.getAttribute('data-ls-as');
        let key = element.getAttribute('data-ls-key') || '$index';
        let limit = parseInt(expression.parse(element.getAttribute('data-limit') || '') || -1);
        let debug = element.getAttribute('data-debug') || false;
        let echo = function() {
            let array = container.path(expr);
            let counter = 0;
            array = (!array) ? [] : array;
            let watch = !!(array && array.__proxy);
            while (element.hasChildNodes()) {
                element.removeChild(element.lastChild);
                element.lastChild = null;
            }
            if (array instanceof Array && typeof array !== 'object') {
                throw new Error('Reference value must be array or object. ' + (typeof array) + ' given');
            }
            let children = [];
            element.$lsSkip = true;
            element.style.visibility = (0 === array.length) ? 'hidden' : 'visible';
            for (let prop in array) {
                if (counter == limit) {
                    break;
                }
                counter++;
                if (!array.hasOwnProperty(prop)) {
                    continue;
                }
                children[prop] = template.cloneNode(true);
                element.appendChild(children[prop]);
                (index => {
                    let context = expr + '.' + index;
                    container.addNamespace(as, context);
                    if (debug) {
                        console.info('debug-ls-loop', 'index', index);
                        console.info('debug-ls-loop', 'context', context);
                        console.info('debug-ls-loop', 'context-path', container.path(context).name);
                        console.info('debug-ls-loop', 'namespaces', container.namespaces);
                    }
                    container.set(as, container.path(context), true, watch);
                    container.set(key, index, true, false);
                    view.render(children[prop]);
                    container.removeNamespace(as);
                })(prop);
            }
            element.dispatchEvent(new Event('looped'));
        };
        let template = (element.children.length === 1) ? element.children[0] : window.document.createElement('li');
        echo();
        container.bind(element, expr + '.length', echo);
        let path = (expr + '.length').split('.');
        while (path.length) {
            container.bind(element, path.join('.'), echo);
            path.pop();
        }
    }
});
window.ls.container.get('view').add({
    selector: 'data-ls-template',
    template: false,
    repeat: false,
    controller: function(element, view, http, expression, document, container) {
        let template = element.getAttribute('data-ls-template') || '';
        let type = element.getAttribute('data-type') || 'url';
        let debug = element.getAttribute('data-debug') || false;
        let paths = [];
        let check = function(init = false) {
            let source = expression.parse(template);
            paths = expression.getPaths();
            element.innerHTML = '';
            if ('script' === type) {
                let inlineTemplate = document.getElementById(source);
                if (inlineTemplate && inlineTemplate.innerHTML) {
                    element.innerHTML = inlineTemplate.innerHTML;
                    element.dispatchEvent(new CustomEvent('template-loaded', {
                        bubbles: true,
                        cancelable: false
                    }));
                } else {
                    if (debug) {
                        console.error('Missing template "' + source + '"');
                    }
                }
                if (!init) {
                    view.render(element);
                }
                return;
            }
            http.get(source).then(function(element) {
                return function(data) {
                    element.innerHTML = data;
                    view.render(element);
                    element.dispatchEvent(new CustomEvent('template-loaded', {
                        bubbles: true,
                        cancelable: false
                    }));
                }
            }(element), function() {
                throw new Error('Failed loading template');
            });
        };
        check(true);
        for (let i = 0; i < paths.length; i++) {
            let path = paths[i].split('.');
            while (path.length) {
                container.bind(element, path.join('.'), check);
                path.pop();
            }
        }
    }
});
